/*
 * Copyright 2011 android-jar-fixer authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.perunlabs.tool.fixandroidjar;

import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_STATIC;

import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * ClassAdapter that rewrites class bytecode by removing 'throw new
 * RuntimeException("Stub!")' calls from methods and constructors.
 */
public class FixClassClassAdapter extends ClassAdapter {
  private String className;
  private String superClassName;
  private boolean isInterface;
  private boolean nonArgumentConstructorFound;

  public FixClassClassAdapter(ClassVisitor cv) {
    super(cv);
    this.nonArgumentConstructorFound = false;
  }

  @Override
  public void visit(int version, int access, String name, String signature, String superName,
      String[] interfaces) {
    this.className = name;
    this.superClassName = superName;
    this.isInterface = (Opcodes.ACC_INTERFACE & access) != 0;
    cv.visit(version, removeFinalAccessModifier(access), name, signature, superName, interfaces);
  }

  @Override
  public FieldVisitor visitField(int access, String name, String desc, String signature,
      Object value) {
    // remove final modifier from non-static fields
    if ((access & ACC_STATIC) == 0) {
      return super.visitField((~ACC_FINAL) & access, name, desc, signature, value);
    }
    return super.visitField(access, name, desc, signature, value);
  }

  @Override
  public MethodVisitor visitMethod(int access, String name, String desc, String signature,
      String[] exceptions) {
    MethodVisitor methodVisitor = cv.visitMethod(removeFinalAccessModifier(access), name, desc,
        signature, exceptions);
    if (methodVisitor == null) {
      return null;
    }
    if (name.equals("<init>")) {
      if (Type.getArgumentTypes(desc).length == 0) {
        nonArgumentConstructorFound = true;
      }
      return new FixConstructorMethodVisitor(methodVisitor);
    } else {
      return new FixMethodMethodVisitor(name, desc, methodVisitor);
    }
  }

  @Override
  public void visitEnd() {
    if (!isInterface) {
      if (!nonArgumentConstructorFound) {
        generatePrivateNoArgConstructor();
      }
      generateCreateFakeInstanceMethod();
    }
  }

  private void generateCreateFakeInstanceMethod() {
    MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC,
        "createFakeInstance", "()L" + className + ";", null, null);
    mv.visitCode();
    mv.visitTypeInsn(Opcodes.NEW, className);
    mv.visitInsn(Opcodes.DUP);
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, className, "<init>", "()V");
    mv.visitInsn(Opcodes.ARETURN);
    mv.visitMaxs(2, 0);
    mv.visitEnd();
  }

  private void generatePrivateNoArgConstructor() {
    MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PRIVATE, "<init>", "()V", null, null);
    mv.visitCode();
    Label label0 = new Label();
    mv.visitLabel(label0);
    mv.visitVarInsn(Opcodes.ALOAD, 0);
    mv.visitMethodInsn(Opcodes.INVOKESPECIAL, superClassName, "<init>", "()V");
    mv.visitInsn(Opcodes.RETURN);
    Label label1 = new Label();
    mv.visitLabel(label1);
    mv.visitLocalVariable("this", "L" + className + ";", null, label0, label1, 0);
    mv.visitMaxs(1, 1);
    mv.visitEnd();
  }

  private static int removeFinalAccessModifier(int access) {
    return access & (~Opcodes.ACC_FINAL);
  }
}
